import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:fluttercommon/fluttercommon.dart';
import 'package:fluttercommon/src/global.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';

import '../extentions/string_extentions.dart';
import '../log/logger.dart';
import 'http_configs.dart';

abstract class ApiContext {
  String get httpRoot;

  Configs get configs;

  String get token;

  bool get appendParametersToUrl => false;

  bool get logPrintCurl => false;

  bool get logPrintParsedResult => false;

  bool get logPrintBodyString => true && configs.isDebug;

  /// common header for all request.
  Map<String, String> createCommonHeader(String url) {
    return {};
  }

  ///derived class should override this method
  Future<Map<String, dynamic>> createCommonParameters(String url) async {
    return {};
  }

  List<String>? getCommonParameterKeys() => null;

  ///derived class should override this method
  dynamic createRequestBody(
    String url,
    String? body,
    Map<String, dynamic> finalParams, {
    bool requestDataAsBody = false,
  }) {
    if (requestDataAsBody == true || this.appendParametersToUrl == false)
      return body ?? finalParams;
    return null;
  }

  ///derived class should override this method
  String createFinalUrl(
    String url,
    bool fullUrl,
    bool fakeData,
  ) {
    if (fullUrl) return url;
    return fakeData ? "${configs.fakeDataRoot}$url.json" : "$httpRoot$url";
  }

  Future<ApiRequestResult> get(
    String url, {
    Map<String, dynamic>? params,
    String? dataKey,
    bool fakeData = false,
    bool fullUrl = false,
    bool decodeWithUtf8 = true,
    Map<String, String>? header,
    bool bodyAsData = false,
    BuildContext? context,
  }) async {
    String finalUrl = createFinalUrl(url, fullUrl, fakeData);

    final headers = createCommonHeader(finalUrl);

    if (header?.isNotEmpty ?? false) {
      headers.addAll(header!);
    }

    final Map<String, dynamic> finalParams =
        (await createCommonParameters(url));
    params?.forEach((k, v) {
      if (v != null) {
        finalParams[k] = v.toString();
      }
    });

    finalUrl = finalUrl.appendQueryParameterToUrl(finalParams);

    printRequestLog(finalUrl, headers, finalParams, null);

    final HttpLog? httpLog;
    if (configs.addToLogger) {
      httpLog = HttpLog()
        ..url = url
        ..params = finalParams
        ..headers = headers;
    } else {
      httpLog = null;
    }

    final now = DateTime.now();
    http.Response? response;
    try {
      if (fakeData) {
        response = await http.get(Uri.parse(url));
      } else {
        response = await http.get(Uri.parse(finalUrl), headers: headers);
      }

      String bodyString = decodeWithUtf8 == true
          ? Encoding.getByName("utf-8")!.decode(response.bodyBytes)
          : response.body;

      if (logPrintBodyString) {
        debugPrint("response of -> $finalUrl");
        final commonKeys = getCommonParameterKeys();
        if (commonKeys?.isNotEmpty ?? false) {
          _printExtraParams(finalParams, commonKeys);
        } else {
          debugPrint("params: $finalParams");
        }
        debugPrint("bodyString = $bodyString");
      }

      final result = ApiRequestResult(
        body: bodyString,
        httpCode: response.statusCode,
        dataKey: dataKey,
        bodyAsData: fullUrl || bodyAsData,
        //// 第三方接口
        parser: resultParser,
      );

      if (logPrintParsedResult)
        printResponseLog(now, finalUrl, finalParams, result);
      if (logPrintCurl || configs.addToLogger) {
        final curl = buildCurl("GET", null, finalParams, headers, finalUrl);
        if (logPrintCurl) {
          print(curl);
        }
        if (configs.addToLogger && httpLog != null) {
          httpLog.curl = curl;
        }
      }

      if (!result.success()) {
        interceptException(context, result);
      }

      if (configs.addToLogger && httpLog != null) {
        httpLog
          ..statusCode = response.statusCode
          ..response = jsonDecode(bodyString)
          ..totalTime = DateTime.now().millisecondsSinceEpoch -
              now.millisecondsSinceEpoch;
        Logger.save(httpLog);
      }
      return result;
    } catch (t, s) {
      final result = ApiRequestResult(
          body: response?.body ?? "", httpCode: response?.statusCode ?? -1);

      if (t is SocketException) {
        result._resultInfo!.message = t.toString();
      }

      printErrorLog(t, s, result);

      if (configs.addToLogger && httpLog != null) {
        httpLog
          ..statusCode = response?.statusCode ?? -1
          ..totalTime = DateTime.now().millisecondsSinceEpoch -
              now.millisecondsSinceEpoch;
        Logger.save(httpLog);
      }

      return result;
    }
  }

  Future<ApiRequestResult> post(
    String url, {
    Map<String, dynamic>? params,
    String? body,
    String? dataKey,
    bool fakeData = false,
    bool fullUrl = false,
    bool decodeWithUtf8 = true,
    Map<String, String>? header,
    bool bodyAsData = false,
    bool requestDataAsBody = false,
    BuildContext? context,
  }) async {
    String finalUrl = createFinalUrl(url, fullUrl, fakeData);

    final headers = createCommonHeader(finalUrl);

    if (header?.isNotEmpty ?? false) {
      headers.addAll(header!);
    }

    final Map<String, dynamic> finalParams =
        (await createCommonParameters(url));
    params?.forEach((k, v) {
      if (v != null) {
        finalParams[k] = v.toString();
      }
    });

    if (appendParametersToUrl == true) {
      finalUrl = finalUrl.appendQueryParameterToUrl(finalParams);
    }

    printRequestLog(finalUrl, headers, finalParams, body);

    final HttpLog? httpLog;
    if (configs.addToLogger) {
      httpLog = HttpLog()
        ..url = url
        ..params = finalParams
        ..headers = headers;
      if (body != null) {
        try {
          httpLog.body = jsonDecode(body);
        } catch (e) {
          httpLog.body = jsonDecode('{"body": $body}');
        }
      }
    } else {
      httpLog = null;
    }

    final now = DateTime.now();
    http.Response? response;
    try {
      if (fakeData) {
        response = await http.get(Uri.tryParse(finalUrl)!);
      } else {
        final requestBody = createRequestBody(url, body, finalParams,
            requestDataAsBody: requestDataAsBody);
        response = await http.post(Uri.parse(finalUrl),
            headers: headers, body: requestBody);
      }

      String bodyString = decodeWithUtf8 == true
          ? Encoding.getByName("utf-8")!.decode(response.bodyBytes)
          : response.body;

      if (logPrintBodyString) {
        debugPrint("response of -> $finalUrl");
        final commonKeys = getCommonParameterKeys();
        if (commonKeys?.isNotEmpty ?? false) {
          _printExtraParams(finalParams, commonKeys);
        } else {
          debugPrint("params: $finalParams");
        }
        debugPrint("bodyString = $bodyString");
      }

      final result = ApiRequestResult(
        body: bodyString,
        httpCode: response.statusCode,
        dataKey: dataKey,
        bodyAsData: fullUrl || bodyAsData,
        //// 第三方接口
        parser: resultParser,
      );

      if (logPrintParsedResult)
        printResponseLog(now, finalUrl, finalParams, result);

      if (logPrintCurl || configs.addToLogger) {
        final curl = buildCurl("POST", body, finalParams, headers, finalUrl);
        if (logPrintCurl) {
          print(curl);
        }
        if (configs.addToLogger && httpLog != null) httpLog.curl = curl;
      }

      if (!result.success()) {
        interceptException(context, result);
      }
      if (configs.addToLogger && httpLog != null) {
        httpLog
          ..statusCode = response.statusCode
          ..response = jsonDecode(bodyString)
          ..totalTime = DateTime.now().millisecondsSinceEpoch -
              now.millisecondsSinceEpoch;
        Logger.save(httpLog);
      }
      return result;
    } catch (t, s) {
      final result = ApiRequestResult(
          body: response?.body ?? "", httpCode: response?.statusCode ?? -1);

      if (t is SocketException) {
        result._resultInfo!.message =
            CustomizableFeatures.strings.networkUnavailable;
      }

      printErrorLog(t, s, result);

      if (configs.addToLogger && httpLog != null) {
        httpLog
          ..statusCode = response?.statusCode ?? -1
          ..totalTime = DateTime.now().millisecondsSinceEpoch -
              now.millisecondsSinceEpoch;
        Logger.save(httpLog);
      }
      return result;
    }
  }

  void printErrorLog(dynamic t, StackTrace s, ApiRequestResult? result) {
    if (configs.isDebug) {
      if (t != null) print(t);
      print(s);
      if (result != null) print(result);
    }
  }

  void printRequestLog(
    String finalUrl,
    Map<String, String> headers,
    Map<String, dynamic> finalParams,
    String? body,
  ) {
    if (configs.isDebug) {
      print(finalUrl);
      print("headers: $headers");

      if (finalParams.isNotEmpty) {
        final commonKeys = getCommonParameterKeys();
        if (commonKeys?.isNotEmpty ?? false) {
          _printExtraParams(finalParams, commonKeys);
        } else {
          debugPrint("params: $finalParams");
        }
      }

      if (body != null) {
        debugPrint("body: $body");
      }
    }
  }

  void _printExtraParams(
      Map<String, dynamic> finalParams, List<String>? commonKeys) {
    final sb = StringBuffer();
    sb.write("params: {");
    for (String k in finalParams.keys) {
      if (!commonKeys!.contains(k)) {
        final v = finalParams[k];
        if (k.toLowerCase().contains("time")) {
          final t = v is int ? v : int.tryParse(v);
          if (t != null) {
            String info = DateTime.fromMillisecondsSinceEpoch(t)
                .format(timePatternYearMonthHourMinuteSecondSlash);
            sb.write("$k = ${finalParams[k]}($info), ");
          } else {
            sb.write("$k = ${finalParams[k]}, ");
          }
        } else {
          sb.write("$k = ${finalParams[k]}, ");
        }
      }
    }
    sb.write("}");
    debugPrint(sb.toString());
  }

  void printResponseLog(
    DateTime startTime,
    String finalUrl,
    Map<String, dynamic> finalParams,
    ApiRequestResult result,
  ) {
    if (configs.isDebug) {
      print(finalUrl +
          " (${DateTime.now().millisecondsSinceEpoch - startTime.millisecondsSinceEpoch}ms)");
      final commonKeys = getCommonParameterKeys();
      if (commonKeys?.isNotEmpty ?? false) {
        _printExtraParams(finalParams, commonKeys);
      } else {
        debugPrint("params: $finalParams");
      }
      debugPrint(result.toString());
    }
  }

  ApiResultInfo resultParser(
    Map<String, dynamic> json, {
    String? dataKey,
    bool? bodyAsData,
  }) {
    return _defaultResultParser(json, dataKey: dataKey, bodyAsData: bodyAsData);
  }

  String buildCurl(String method, String? body, Map<String, dynamic> params,
      Map<String, String> headers, String url) {
    final sb = StringBuffer();
    sb.write(" curl -i -X $method ");
    sb.write('-H "Accept-Encoding:compress;q=0.5, gzip;q=1.0" ');

    if (params.isNotEmpty) {
      final pb = StringBuffer();
      params.forEach((k, v) {
        pb.write("$k=$v&");
      });
      pb.write("xxx=xxx");
      sb.write(" -d \"${pb.toString()}\" ");
    }

    if (body != null) {
      sb.write(" -d $body ");
    }

    if (headers.isNotEmpty) {
      headers.forEach((k, v) {
        sb.write("-H \"$k:$v\" ");
      });
    }

    sb.write(url);

    return sb.toString();
  }

  void interceptException(BuildContext? context, ApiRequestResult result) {
    if (result._resultInfo?.code == HttpExceptionCode.tokenExpired) {
      //authenticationBloc.add(LoggedOut()); todo how to logout ?
    }
  }

  String _parseFilename(File file) {
    var name = file.path;
    if (name.contains("/")) {
      name = name.substring(name.lastIndexOf("/") + 1);
    }
    return name;
  }

  Future<ApiRequestResult> uploadImageFile(
    BuildContext context,
    String url,
    File file, {
    Map<String, dynamic>? params,
    String? dataKey,
  }) async {
    return uploadImageFiles(
      context,
      url,
      [file],
      params: params,
      dataKey: dataKey,
    );
  }

  ///图片文件上传
  Future<ApiRequestResult> uploadImageFiles(
    BuildContext context,
    String url,
    List<File> files, {
    Map<String, dynamic>? params,
    String? dataKey,
  }) async {
    return _uploadImage(
      context,
      url,
      files.map((e) => _parseFilename(e)).toList(),
      await Future.wait(files.map((e) => e.readAsBytes())),
      params: params,
      dataKey: dataKey,
    );
  }

  Future<ApiRequestResult> _uploadImage(
    BuildContext context,
    String url,
    List<String> names,
    List<List<int>> filesListBytes, {
    Map<String, dynamic>? params,
    String? dataKey,
  }) async {
    final String finalUrl = url.startsWith("http") ? url : (httpRoot + url);
    // string to uri
    Uri uri = Uri.parse('$finalUrl');
    final headers = createCommonHeader(finalUrl);
    if (isDebug) print("uploadImageFile to $uri");
// create multipart request
    http.MultipartRequest request = http.MultipartRequest("POST", uri);

    if ((names.isNotEmpty) &&
        (filesListBytes.isNotEmpty) &&
        names.length == filesListBytes.length) {
      for (int i = 0; i < names.length; i++) {
        http.MultipartFile multipartFile = http.MultipartFile.fromBytes(
          'file',
          filesListBytes[i],
          filename: '${names[i]}',
          contentType: MediaType("image", "jpg"),
        );
        request.files.add(multipartFile);
      }
    }

    params?.forEach((key, value) {
      request.fields[key] = '$value';
    });

    http.StreamedResponse? response;
    try {
      request.headers.addAll(headers);
      response = await request.send();
      String responseBody = await response.stream.bytesToString();
      if (logPrintBodyString) debugPrint('upload response $responseBody');
      final result = ApiRequestResult(
        body: responseBody,
        httpCode: response.statusCode,
        dataKey: dataKey,
        parser: resultParser,
      );
      if (configs.isDebug && logPrintParsedResult) {
        print(finalUrl);
        print(result);
      }
      if (!result.success()) {
        interceptException(context, result);
      }
      return result;
    } catch (t, s) {
      printErrorLog(t, s, null);
      return ApiRequestResult(body: "", httpCode: response?.statusCode ?? -1);
    }
  }
}

abstract class ContentTypeConstant {
  static final String json = "application/json";
  static final String xml = "application/xml";
  static final String formUrlEncoded = "application/x-www-form-urlencoded";
  static String headerContentType = "Content-Type";
}

abstract class HttpExceptionCode {
  static final authParamInvalid = 1001; //参数不全
  static final authExpired = 1002; //授权过期
  static final tokenExpired = 10005; //TOKEN失效
  static final accountLocked = 90000; //账号已经冻结
  static final accountNotExist = 20000; //账号不存在

  static final timeout = 20100; //请求超时
  static final noNetwork = 20101; //没有网络
}

class ApiResultInfo {
  bool success;
  int? code;
  String? message;
  dynamic data;
  bool parseDataSuccess;

  ApiResultInfo({
    this.code,
    this.message,
    this.data,
    this.success = false,
    this.parseDataSuccess = true,
  });

  @override
  String toString() {
    return 'ApiResultInfo{status: $success, code: $code, message: $message, parseDataSuccess: $parseDataSuccess, data: $data}'
        "${(success && data != null) ? ('\ndata: ' + jsonEncode(data)) : ('\nbody: ' + (data?.toString() ?? ""))}\n";
  }
}

typedef ApiResultInfo _ApiResultParser(
  Map<String, dynamic> json, {
  String? dataKey,
  bool? bodyAsData,
});

ApiResultInfo _defaultResultParser(
  Map<String, dynamic> json, {
  String? dataKey,
  bool? bodyAsData,
}) {
  final resultInfo = ApiResultInfo();
  if (bodyAsData == true) {
    if (dataKey?.isNotEmpty ?? false) {
      resultInfo.data = json[dataKey!];
    } else {
      resultInfo.data = json;
    }
    resultInfo.parseDataSuccess = true;
    resultInfo.success = true;
  } else {
    try {
      resultInfo.code = json["code"];
      resultInfo.message = json["message"];
      final status = json["status"];
      if (status == "success") {
        resultInfo.success = true;
        if (dataKey != null && dataKey.isNotEmpty) {
          resultInfo.data = json["data"][dataKey];
        } else {
          resultInfo.data = json["data"];
        }
      } else {
        resultInfo.success = false;
        resultInfo.data = json["data"];
      }
    } catch (e, s) {
      resultInfo.parseDataSuccess = false;
      print(s);
    }
  }
  return resultInfo;
}

class ApiRequestResult {
  final String? body;
  final int? httpCode;
  ApiResultInfo? _resultInfo;
  final String? dataKey;
  final bodyAsData;
  final _ApiResultParser parser;
  var _parseDataSuccess = true;

  ApiRequestResult({
    this.body,
    this.httpCode,
    this.dataKey,
    this.bodyAsData = false,
    this.parser = _defaultResultParser,
  }) {
    if (body != null && body?.isNotEmpty == true) {
      try {
        final Map<String, dynamic>? result = jsonDecode(body!);
        if (result != null) {
          _resultInfo =
              parser(result, dataKey: dataKey, bodyAsData: bodyAsData);
        }
      } catch (e, s) {
        print(e);
        print(s);
      }
    }
    _resultInfo ??= ApiResultInfo();
  }

  factory ApiRequestResult.fakeResult() {
    return ApiRequestResult(httpCode: 200).._resultInfo = ApiResultInfo();
  }

  bool success() =>
      _parseDataSuccess && httpCode == 200 && _resultInfo!.success == true;

  dynamic get data => _resultInfo?.data;

  int? get code => _resultInfo?.code;

  @override
  String toString() {
    return "RequestResult:{httpCode: $httpCode, parseDataSuccess: $_parseDataSuccess, resultInfo: $_resultInfo";
  }
}

class ApiResult {
  final ApiRequestResult? rawResult;
  final dynamic data;

  const ApiResult({this.rawResult, this.data});

  bool success() =>
      identical(this, ok) || (rawResult != null && rawResult!.success());

  int? get code => rawResult?.code;

  String message() => rawResult?._resultInfo?.message ?? "";

  static final ApiResult ok = ApiResult();

  factory ApiResult.fakeResult(int code, String message, {dynamic data}) {
    return ApiResult(
        rawResult: ApiRequestResult.fakeResult()
          .._resultInfo?.message = message
          .._resultInfo?.code = code,
        data: data);
  }

  @override
  String toString() {
    return 'ApiResult{success: ${success()}, message: ${message()}, rawResult: $rawResult}';
  }
}
